﻿namespace WebServer.BusinessLogic
{
    using System;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;

    using WebServer.BusinessLogic.Handlers;
    using WebServer.BusinessLogic.Logs;

    public class Communicator : INotifyPropertyChanged, ICommunicator
    {
        #region Static Fields

        /// <summary>
        ///   Create static instance, Singleton, so it could be created only once
        /// </summary>
        private static Communicator _instance;

        #endregion

        #region Fields

        private readonly LoadBalancer _loadBalancer = new LoadBalancer();

        private int _activeConnections;

        private TcpListener _tcpListener;

        #endregion

        #region Public Events

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Public Properties

        public static Communicator Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new Communicator();
                }
                return _instance;
            }
        }

        public int ActiveConnections
        {
            get
            {
                return this._activeConnections;
            }
            private set
            {
                this._activeConnections = value;
                this.OnPropertyChanged("ActiveConnections");
            }
        }

        /// <summary>
        ///   Bool for starting listening
        /// </summary>
        public bool IsActive { get; set; }

        public LoadBalancer LoadBalancer
        {
            get
            {
                return this._loadBalancer;
            }
        }

        #endregion

        #region Public Methods and Operators

        public int GetLoad()
        {
            return this.ActiveConnections * 100 / Configurator.Instance.MaxUsers;
        }

        public Response GetPage(Request request, string remoteServer)
        {
            var ourResponse = new Response();

            string host = !remoteServer.StartsWith("http", StringComparison.OrdinalIgnoreCase)
                              ? "http://" + remoteServer
                              : remoteServer;

            Uri uri;
            if (Uri.TryCreate(host, UriKind.Absolute, out uri))
            {
                HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
                HttpWebResponse response;
                var data = new byte[0];
                try
                {
                    response = (HttpWebResponse)webRequest.GetResponse();
                    data = new byte[response.ContentLength];
                    using (var stream = response.GetResponseStream())
                    {
                        if (stream != null)
                        {
                            stream.Read(data, 0, data.Length);
                        }
                    }
                }
                catch (WebException e)
                {
                    response = ((HttpWebResponse)e.Response);
                }

                if (response != null)
                {
                    ourResponse = new Response(response.ContentType, response.ProtocolVersion.ToString(), data);
                    ourResponse.HttpResponseCode = (int)response.StatusCode;
                    ourResponse.HttpResponseCodeDescription = response.StatusDescription;
                }
            }

            return ourResponse;
        }

        /// <summary>
        ///   Starts listening.
        /// </summary>
        public void StartListening()
        {
            // Set thread max threads.
            int maxThreads = Environment.ProcessorCount * 4;
            ThreadPool.SetMaxThreads(maxThreads, maxThreads);

            //set the active state
            this.IsActive = true;

            //run thread of webserver
            Thread thread = new Thread(this.StartListeningThread);
            //ThreadPool.QueueUserWorkItem();
            thread.Start();

            this._loadBalancer.StartAnsweringThread();

            if (Configurator.Instance.ServerMode == ServerConfiguration.LoadBalancer)
            {
                this._loadBalancer.StartAskingThread();
            }
        }

        /// <summary>
        ///   Stops listening in the while loop
        /// </summary>
        public void StopListening()
        {
            this.IsActive = false;

            if (this._tcpListener != null)
            {
                this._tcpListener.Server.Close();
            }

            this._loadBalancer.StopAnsweringThread();
        }

        public bool TryToFreePortForWebserver()
        {
            this._tcpListener = new TcpListener(Configurator.Instance.Host, Configurator.Instance.Port);
            try
            {
                this._tcpListener.Start();
                return true;
            }
            catch (SocketException)
            {
                Logger.Instance.Log(
                    string.Format(
                        "Error. Server could not restart itself on address: {0}:{1}",
                        Configurator.Instance.Host,
                        Configurator.Instance.Port));
            }
            finally
            {
                this._tcpListener.Server.Close();
            }
            return false;
        }

        #endregion

        #region Methods

        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        ///   Converts response to bytes.
        /// </summary>
        /// <param name="response"> </param>
        /// <returns> </returns>
        private byte[] GetResponseBytes(Response response)
        {
            var responseBuilder = new ResponseBuilder(response);
            var responseBytes = responseBuilder.GetResponseBytes();
            if (responseBytes == null)
            {
                //if response is null, create Server 500 Error:

                var r = new Response();
                r.SetServerError();
                responseBuilder = new ResponseBuilder(r);
                responseBytes = responseBuilder.GetResponseBytes();
            }
            return responseBytes;
        }

        /// <summary>
        ///   Sends formed request object to the handlers
        /// </summary>
        /// <param name="request"> </param>
        /// <returns> Responce </returns>
        private Response HandleRequest(Request request)
        {
            // create Handlers
            var handler0 = new LoadBalancerHandler();
            var handler1 = new SessionHandler();
            var handler2 = new DirectoryHandler();
            var handler3 = new AdminHandler();
            var handler4 = new TextHandler();
            var handler5 = new BinaryHandler();
            var handler6 = new RedirectHandler();
            var handler7 = new ErrorHandler();

            // set Handler sequence of execution
            handler0.SetNext(handler1);
            handler1.SetNext(handler2);
            handler2.SetNext(handler3);
            handler3.SetNext(handler4);
            handler4.SetNext(handler5);
            handler5.SetNext(handler6);
            handler6.SetNext(handler7);

            // handle request
            return handler0.Handle(request);
        }

        /// <summary>
        ///   Handles request and sends response to browser.
        /// </summary>
        /// <param name="tcpSocket"> </param>
        private void ReturnResponse(Socket tcpSocket)
        {
            try
            {
                Logger.Instance.Log(
                    string.Format(
                        "Connected {0}:{1}",
                        ((IPEndPoint)tcpSocket.RemoteEndPoint).Address,
                        ((IPEndPoint)tcpSocket.RemoteEndPoint).Port));
                // keep connection alive and process all the requests from client
                while (this.IsActive)
                {
                    // create container for received data
                    var temp = new byte[tcpSocket.ReceiveBufferSize];

                    // get data from client and return count of read bytes
                    int res = tcpSocket.Receive(temp);
                    if (res == 0)
                    {
                        // no more data from client, he has disconnected from server
                        break;
                    }

                    //Trace.WriteLine("Breaking ;-}");

                    //throw  new SocketException();
                    //break;

                    // create request
                    Request req = new Request();
                    req.ParseRequest(temp);

                    // get IP
                    IPEndPoint ipEndPoint = tcpSocket.RemoteEndPoint as IPEndPoint;
                    if (ipEndPoint != null)
                    {
                        req.UserIpAddress = ipEndPoint.Address.ToString();
                    }

                    Trace.WriteLine("  " + req.HttpPath);

                    // log request
                    Logger.Instance.Log(
                        String.Format(
                            "{0} {1} from {2}; UserAgent: {3}",
                            req.HttpMethod,
                            req.HttpPath,
                            req.UserIpAddress,
                            req.UserAgent));

                    Response response = null;

                    //handle only if correct format of request
                    if (req.IsCorrect)
                    {
                        //get Response
                        response = this.HandleRequest(req);
                    }
                    else
                    {
                        // respond with error message
                        response = new Response();
                        response.MimeType = "text/html";
                        response.Data =
                            Encoding.UTF8.GetBytes("Request was not in correct format, please fix your browser!");
                    }

                    //TODO: check if Response.IsCorrect?

                    //form Response
                    var responseBytes = this.GetResponseBytes(response);

                    //send Response to the web browser
                    this.SendToBrowser(responseBytes, ref tcpSocket);
                }
            }

            catch (SocketException)
            {
                Logger.Instance.Log("SocketException happened in Communicator");
            }
            finally
            {
                this.ActiveConnections--;
                //Interlocked.Decrement(ref _activeConnections);

                var ipEndPoint = tcpSocket.RemoteEndPoint as IPEndPoint;
                if (ipEndPoint != null)
                {
                    Logger.Instance.Log(string.Format("Disconnecting {0}:{1}", ipEndPoint.Address, ipEndPoint.Port));
                }

                //try closing socket
                this.TryClosingSocket(tcpSocket);
            }
        }

        private void ReturnResponseThreadProc(Object tcpSocket)
        {
            Stopwatch sw = new Stopwatch();
            sw.Reset();
            sw.Start();
            this.ReturnResponse((Socket)tcpSocket);
            sw.Stop();
            Trace.WriteLine(string.Format("Took {0} ms", sw.ElapsedMilliseconds));
        }

        /// <summary>
        ///   Sends data to browser.
        /// </summary>
        /// <param name="responseBytes"> </param>
        /// <param name="tcpSocket"> </param>
        private void SendToBrowser(byte[] responseBytes, ref Socket tcpSocket)
        {
            //send data back to opened socket
            tcpSocket.Send(responseBytes);
        }

        /// <summary>
        ///   Starts listening thread
        /// </summary>
        private void StartListeningThread()
        {
            for (int i = Configurator.Instance.Port; i < 65000; i++)
            {
                if (this.TryToFreePortForWebserver())
                {
                    //port free'd
                    break;
                }

                //try to change port
                Configurator.Instance.Port++;
            }

            this._tcpListener = new TcpListener(Configurator.Instance.Host, Configurator.Instance.Port);
            this._tcpListener.Start();

            while (this.IsActive)
            {
                try
                {
                    Socket tcpSocket = this._tcpListener.AcceptSocket();
                    if (tcpSocket.Connected)
                    {
                        this.ActiveConnections++;
                        ThreadPool.QueueUserWorkItem(this.ReturnResponseThreadProc, tcpSocket);
                    }
                }
                catch (SocketException e)
                {
                    Logger.Instance.Log("Error while AcceptSocket: " + e.Message);
                }

                catch
                {
                }
            }
        }

        private void TryClosingSocket(Socket tcpSocket)
        {
            try
            {
                if (tcpSocket.Connected)
                {
                    tcpSocket.Shutdown(SocketShutdown.Both);
                    tcpSocket.Close();
                }
            }
            catch (SocketException)
            {
                Trace.WriteLine("forseclosed");
            }
        }

        #endregion
    }
}